ATOM Documentation

← Back to App

API Response Standard

Overview

This document defines the standard API response format for all ATOM SaaS API routes. Consistent response formats improve developer experience, simplify error handling, and ensure proper monitoring.

Standard Response Format

Success Response

{
  "data": T,           // Response data
  "timestamp": string   // ISO 8601 timestamp
}

Error Response

{
  "error": string,     // Error message
  "code"?: string,     // Error code for programmatic handling
  "details"?: unknown, // Additional error details
  "timestamp": string  // ISO 8601 timestamp
}

Implementation

All API routes MUST use the helpers from @/lib/api/api-response:

import { sendApiError, sendApiSuccess, withApiHandler, withTenantContext, Errors } from '@/lib/api/api-response'

// ✅ GOOD: Using helpers
export async function GET(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      const result = await getData(tenantId)
      return sendApiSuccess(result)
    }, request)
  })
}

// ❌ BAD: Manual NextResponse.json
export async function GET(request: Request) {
  try {
    const result = await getData()
    return NextResponse.json(result)
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Error Helpers

Use the Errors object for common errors:

import { Errors } from '@/lib/api/api-response'

// Quick error creation
throw Errors.unauthorized('You must log in')
throw Errors.forbidden('Access denied')
throw Errors.notFound('Agent')
throw Errors.badRequest('Invalid input')
throw Errors.conflict('Resource already exists')
throw Errors.rateLimited()
throw Errors.internal('Something went wrong')
throw Errors.validation({ field: 'error' })
throw Errors.paymentRequired('Upgrade required')

Route Handler Wrapper

Use withApiHandler for automatic error handling:

export async function POST(request: Request) {
  return withApiHandler(async () => {
    const body = await request.json()
    // Errors automatically caught and formatted
    const result = await processData(body)
    return sendApiSuccess(result)
  })
}

Tenant Context Wrapper

Use withTenantContext for guaranteed tenant isolation:

export async function GET(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      // Guaranteed to have tenant context here
      const agents = await db.query(
        'SELECT * FROM agents WHERE tenant_id = $1',
        [tenantId]
      )
      return sendApiSuccess(agents.rows)
    }, request)
  })
}

Rate Limiting

Use withRateLimit for rate-limited routes:

export async function POST(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      return withRateLimit(async () => {
        const result = await expensiveOperation()
        return sendApiSuccess(result)
      }, tenantId, redis)
    }, request)
  })
}

Migration Guide

Before (Non-Standard)

export async function GET(req: NextRequest) {
    try {
        const session = await getServerSession(authOptions)
        if (!session) {
            return NextResponse.json(
                { error: 'Unauthorized', code: 'UNAUTHORIZED' },
                { status: 401 }
            )
        }

        const tenant = await getTenantFromRequest(req)
        if (!tenant) {
            return NextResponse.json(
                { error: 'Tenant not found', code: 'TENANT_NOT_FOUND' },
                { status: 404 }
            )
        }

        const result = await getData(tenant.id)
        return NextResponse.json(result)
    } catch (error) {
        console.error('Failed:', error)
        return NextResponse.json(
            { error: 'Internal server error', code: 'INTERNAL_ERROR' },
            { status: 500 }
        )
    }
}

After (Standard)

export async function GET(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      const result = await getData(tenantId)
      return sendApiSuccess(result)
    }, request)
  })
}

Common Error Codes

CodeStatusDescription
UNAUTHORIZED401Authentication required
FORBIDDEN403Access denied
NOT_FOUND404Resource not found
BAD_REQUEST400Invalid request
VALIDATION_ERROR400Request validation failed
CONFLICT409Resource conflict
RATE_LIMITED429Rate limit exceeded
INTERNAL_ERROR500Server error
TENANT_NOT_FOUND404Tenant not found
TENANT_CONTEXT_ERROR500Failed to resolve tenant
PAYMENT_REQUIRED402Payment required
LIMIT_EXCEEDED403Tier limit exceeded

Status Codes

Success

  • 200 OK - Successful request
  • 201 Created - Resource created
  • 204 No Content - Successful request with no response body

Client Errors

  • 400 Bad Request - Invalid request
  • 401 Unauthorized - Authentication required
  • 402 Payment Required - Payment required
  • 403 Forbidden - Access denied
  • 404 Not Found - Resource not found
  • 409 Conflict - Resource conflict
  • 429 Too Many Requests - Rate limit exceeded

Server Errors

  • 500 Internal Server Error - Server error
  • 503 Service Unavailable - Service unavailable

Best Practices

1. Always Use Helpers

✅ Use sendApiError, sendApiSuccess, withApiHandler

❌ Don't manually create NextResponse.json

2. Include Error Codes

✅ Use descriptive error codes like AGENT_NOT_FOUND

❌ Don't use generic codes like ERROR

3. Provide Context

✅ Include relevant details in error responses

❌ Don't expose sensitive information in production

4. Use Timestamps

✅ Always include ISO 8601 timestamps

❌ Don't omit timestamps from responses

5. Handle Errors Gracefully

✅ Use try-catch or withApiHandler wrapper

❌ Don't let errors propagate unhandled

Testing

Test Success Response

const response = await fetch('/api/agents')
const data = await response.json()

expect(data).toHaveProperty('data')
expect(data).toHaveProperty('timestamp')
expect(response.status).toBe(200)

Test Error Response

const response = await fetch('/api/agents/invalid')
const data = await response.json()

expect(data).toHaveProperty('error')
expect(data).toHaveProperty('code')
expect(data).toHaveProperty('timestamp')
expect(response.status).toBe(404)

Monitoring

All API responses should be monitored for:

  • Error rates by code
  • Response times
  • Rate limit violations
  • Tenant context resolution failures

Enforcement

Linter Rule

A custom ESLint rule should enforce:

  1. Use of sendApiError for error responses
  2. Use of sendApiSuccess for success responses
  3. Use of withApiHandler wrapper
  4. Inclusion of timestamps

Pre-Commit Hook

A pre-commit hook should check:

  1. No direct NextResponse.json calls with error status codes
  2. All API routes use standard helpers

Examples

Complete Example

import { sendApiError, sendApiSuccess, withApiHandler, withTenantContext, Errors } from '@/lib/api/api-response'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      const agents = await db.query(
        'SELECT * FROM agents WHERE tenant_id = $1',
        [tenantId]
      )
      return sendApiSuccess(agents.rows)
    }, request)
  })
}

export async function POST(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      const body = await request.json()

      if (!body.name) {
        throw Errors.badRequest('Name is required')
      }

      const agent = await createAgent(tenantId, body)
      return sendApiSuccess(agent, 201)
    }, request)
  })
}

References

  • Implementation: src/lib/api/api-response.ts
  • Examples: src/app/api/agents/[id]/run/route.ts (good example)
  • Migration Guide: See above

Changelog

  • 2026-02-08: Initial standard created
  • 2026-02-08: Migration guide added
  • 2026-02-08: Helper utilities documented